""" 交互式作图函, 调用的库包括pyecharts、bokeh """
from __future__ import annotations
import webbrowser
import pandas as pd
from pyecharts import options as opts
from pyecharts.charts import Bar, Page, Geo, Calendar
from pyecharts.globals import GeoType

from PyQt6.QtCore import QThread, pyqtSignal
from bokeh.models import (ColumnDataSource, FactorRange, SingleIntervalTicker,
                          DatetimeTickFormatter, DatetimeTicker)
from bokeh.plotting import figure, show, output_file
from bokeh.layouts import gridplot
from bokeh.palettes import Category10, Category20, Turbo256, linear_palette

import _config as cfg


def calendar_heatmap(data_: pd.Series, path_html: str):
    """ 日历热力图 
        data_: AQI的日均值数据
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-06
    """

    # 按年分组
    group_yearly = data_.groupby(pd.Grouper(freq='Y'))

    # 包含的年列表
    list_year = [year for year, series_year in group_yearly]

    # 创建一个 Page 实例，将多个图表放在同一页上
    if len(list_year) > 5:
        page = Page(layout=Page.SimplePageLayout, page_title='Calendar Heatmap', is_remove_br=True)
    else:
        page = Page(interval=0, page_title='Calendar Heatmap', is_remove_br=True)

    # 按年作图
    for year, series_year in group_yearly:
        # 图片保存默认文件名
        png_name = 'AQI_' + year.strftime('%Y')

        # 整理数据
        data_year = list(zip(series_year.index.strftime('%Y/%m/%d'), series_year))
        # print(data_year)

        # ToolboxOpts工具箱选项-下载按钮设置
        opts_toolbox = opts.ToolboxOpts(
            is_show=True,
            orient='horizontal',
            pos_left='95%',
            pos_top='2%',
            feature=opts.ToolBoxFeatureOpts(
                save_as_image=opts.ToolBoxFeatureSaveAsImageOpts(
                    type_='png',
                    background_color='rgba(255, 255, 255, 0)',
                    name=png_name,
                    pixel_ratio=3,
                ),
                restore=opts.ToolBoxFeatureRestoreOpts(is_show=False),
                data_view=opts.ToolBoxFeatureDataViewOpts(is_show=False),
                data_zoom=opts.ToolBoxFeatureDataZoomOpts(is_show=False),
                magic_type=opts.ToolBoxFeatureMagicTypeOpts(is_show=False),
            ),
        )

        # VisualMapOpts颜色映射选项
        opts_visual_map = opts.VisualMapOpts(
            range_text=['', 'AQI'],
            range_color=['#66c430', '#e9da2e', '#f57217', '#ee1c25', '#66247b', '#8a2327'],
            max_=500,
            min_=1,
            pieces=[
                    {"min": 301, "max": 500},
                    {"min": 201, "max": 300},
                    {"min": 151, "max": 200},
                    {"min": 101, "max": 150},
                    {"min": 51, "max": 100},
                    {"min": 0, "max": 50}
            ],
            orient="vertical",
            is_piecewise=True,
            is_inverse=True,
            pos_left='87%',
            pos_bottom='4%',
        )

        # 创建Calendar实例
        calendar_year = Calendar(init_opts=opts.InitOpts(width="1200px", height="180px"))

        # 日历图设置
        opts_calendar = opts.CalendarOpts(
            range_=year.strftime('%Y'),
            monthlabel_opts=opts.CalendarMonthLabelOpts(name_map='EN', label_font_weight='bold'),
            daylabel_opts=opts.CalendarDayLabelOpts(name_map='EN', label_font_weight='bold', first_day=1),
            splitline_opts=opts.SplitLineOpts(is_show=True, linestyle_opts=opts.LineStyleOpts(width=2, color='black')),
            height='150px',
            width='980px',
            pos_bottom='0%',
            pos_top='10%',
            pos_left='5%',
            itemstyle_opts=opts.ItemStyleOpts(color='rgba(255, 255, 255, 0)'),
        )

        # 添加作图数据
        calendar_year.add(series_name="", yaxis_data=data_year, calendar_opts=opts_calendar)

        # 全局设置
        calendar_year.set_global_opts(
            legend_opts=opts.LegendOpts(is_show=False),
            visualmap_opts=opts_visual_map,
            toolbox_opts=opts_toolbox,
            tooltip_opts=opts.TooltipOpts(formatter='{c}'),
        )

        # 隐藏标记值
        calendar_year.set_series_opts(label_opts=opts.LabelOpts(is_show=False))

        # 添加到Page
        page.add(calendar_year)

    # 生成作图html文件
    page.render(path_html)

    # 使用默认浏览器打开HTML文件
    webbrowser.open(path_html)


def aqi_bar_by_city_yearly(data_: dict, path_html: str):
    """ AQI分城市及污染程度统计
        data_: key为城市名, value为df, df的index为年, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-06
    """

    # 创建一个Page实例，将多个图表放在同一页上
    page = Page(layout=Page.SimplePageLayout, page_title='AQI Bar-Year', is_remove_br=True)

    """ 选项 """
    # 底部x轴选项
    opts_xaxis_bottom = opts.AxisOpts(
        position='bottom',
        axislabel_opts=opts.LabelOpts(
            rotate=45,
            horizontal_align='center',
            margin=20,
            font_weight='bold',
            font_family='Microsoft YaHei'
        ),
        axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=False),
    )

    # # 顶部x轴选项
    # opts_xaxis_top = opts.AxisOpts(
    #     axisline_opts=opts.AxisLineOpts(is_on_zero=False, linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
    #     axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
    # )

    # 左y轴选项
    opts_yaxis_left = opts.AxisOpts(
        type_="value",
        name="Percentage (%)",
        name_location='center',
        name_gap=30,
        min_=0,
        max_=100.2,
        axislabel_opts=opts.LabelOpts(font_weight='bold', font_family='Microsoft YaHei'),
        axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=False),
        name_textstyle_opts=opts.TextStyleOpts(font_weight='bold', font_family='Microsoft YaHei'),
        minor_tick_opts=opts.MinorTickOpts(is_show=True, split_number=2),
    )

    # 右y轴选项
    # opts_yaxis_right = opts.AxisOpts(axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')))

    # 按城市作图
    for city in data_.keys():

        # 创建一个 Bar 实例
        bar_city = Bar(init_opts=opts.InitOpts(width='480px', height='320px'))

        # 年份列表
        list_year = data_[city].index.strftime('%Y').tolist()

        # 添加x轴数据
        bar_city.add_xaxis(xaxis_data=list_year)

        # 默认保存文件名
        png_name = 'AQI_' + city

        """ 添加不同污染程度的数据 """
        # 添加数据
        for i in range(len(cfg.list_pollution_level)):
            bar_city.add_yaxis(series_name=cfg.list_pollution_level[i],
                               y_axis=data_[city].iloc[:, i].astype(str).tolist(),
                               stack='stack_' + city,
                               color=cfg.list_color_pollution_level[i],
                               bar_width='75%',
                               )

        # 添加顶部x轴
        # bar_city.extend_axis(xaxis=opts_xaxis_top)

        # 添加右y轴
        # bar_city.extend_axis(yaxis=opts_yaxis_right)

        # ToolboxOpts工具箱选项
        opts_toolbox = opts.ToolboxOpts(
            is_show=True,
            orient='horizontal',
            pos_left='93%',
            pos_top='2%',
            feature=opts.ToolBoxFeatureOpts(
                save_as_image=opts.ToolBoxFeatureSaveAsImageOpts(
                    type_='png',
                    background_color='rgba(255, 255, 255, 0)',
                    name=png_name,
                    pixel_ratio=4,
                ),
                restore=opts.ToolBoxFeatureRestoreOpts(is_show=False),
                data_view=opts.ToolBoxFeatureDataViewOpts(is_show=False),
                data_zoom=opts.ToolBoxFeatureDataZoomOpts(is_show=False),
                magic_type=opts.ToolBoxFeatureMagicTypeOpts(is_show=False),
            ),
        )

        # 序列label选项
        bar_city.set_series_opts(label_opts=opts.LabelOpts(is_show=False))

        # legend设置
        opts_legend = opts.LegendOpts(
            item_width=15, border_color='transparent', pos_left='center', pos_top='9%',
            textstyle_opts=opts.TextStyleOpts(color='black'),
        )

        # 全局设置
        bar_city.set_global_opts(
            title_opts=opts.TitleOpts(title=city, pos_left='center', title_textstyle_opts=opts.TextStyleOpts(color='black')),  # 标题设置
            yaxis_opts=opts_yaxis_left,
            datazoom_opts=opts.DataZoomOpts(is_show=False, type_='slider', range_start=0, range_end=100),
            toolbox_opts=opts_toolbox,
            legend_opts=opts_legend,
            xaxis_opts=opts_xaxis_bottom,

        )

        # bar添加至page中
        page.add(bar_city)

    # 渲染页面
    page.render(path_html)

    # 打开图片
    webbrowser.open(path_html)


def aqi_bar_by_city_seasonal(data_: dict, path_html: str):
    """ AQI分城市及污染程度统计
        data_: key为城市名, value为df, df的index为年-月, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-06
    """

    # 创建一个Page实例，将多个图表放在同一页上
    page = Page(layout=Page.SimplePageLayout, page_title='AQI Bar-Season', is_remove_br=True)

    """ 选项 """
    # 底部x轴选项
    opts_xaxis_bottom = opts.AxisOpts(
        position='bottom',
        axislabel_opts=opts.LabelOpts(
            rotate=90,
            horizontal_align='right',
            margin=10,
            font_weight='bold',
            font_family='Microsoft YaHei'
        ),
        axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=False),
    )

    # 顶部x轴选项
    # opts_xaxis_top = opts.AxisOpts(
    #     axisline_opts=opts.AxisLineOpts(is_on_zero=False, linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
    #     axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
    # )

    # 左y轴选项
    opts_yaxis_left = opts.AxisOpts(
        type_="value",
        name="Percentage (%)",
        name_location='center',
        name_gap=40,
        min_=0,
        max_=100.2,
        axislabel_opts=opts.LabelOpts(font_weight='bold', font_family='Microsoft YaHei'),
        axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=False),
        name_textstyle_opts=opts.TextStyleOpts(font_weight='bold', font_family='Microsoft YaHei'),
        minor_tick_opts=opts.MinorTickOpts(is_show=True, split_number=2),
    )

    # 右y轴选项
    # opts_yaxis_right = opts.AxisOpts(axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')))

    # 按城市作图
    for city in data_.keys():

        # 创建一个 Bar 实例

        # bar_city = Bar()
        bar_city = Bar(init_opts=opts.InitOpts(width='800px', height='400px', is_horizontal_center=True))

        # 添加x轴数据
        bar_city.add_xaxis(xaxis_data=data_[city].index.astype(str).tolist())

        # 默认保存文件名
        png_name = 'AQI_' + city

        """ 添加不同污染程度的数据 """
        # 添加数据
        for i in range(len(cfg.list_pollution_level)):
            bar_city.add_yaxis(series_name=cfg.list_pollution_level[i],
                               y_axis=data_[city].iloc[:, i].astype(str).tolist(),
                               stack='stack_' + city,
                               color=cfg.list_color_pollution_level[i],
                               bar_width='100%',
                               )

        # 添加顶部x轴
        # bar_city.extend_axis(xaxis=opts_xaxis_top)

        # 添加右y轴
        # bar_city.extend_axis(yaxis=opts_yaxis_right)

        # ToolboxOpts工具箱选项
        opts_toolbox = opts.ToolboxOpts(
            is_show=True,
            orient='horizontal',
            pos_left='92%',
            pos_top='2%',
            feature=opts.ToolBoxFeatureOpts(
                save_as_image=opts.ToolBoxFeatureSaveAsImageOpts(
                    type_='png',
                    background_color='rgba(255, 255, 255, 0)',
                    name=png_name,
                    pixel_ratio=4,
                ),
                restore=opts.ToolBoxFeatureRestoreOpts(is_show=False),
                data_view=opts.ToolBoxFeatureDataViewOpts(is_show=False),
                data_zoom=opts.ToolBoxFeatureDataZoomOpts(is_show=False),
                magic_type=opts.ToolBoxFeatureMagicTypeOpts(is_show=False),
            ),
        )

        # 序列label选项
        bar_city.set_series_opts(label_opts=opts.LabelOpts(is_show=False))

        # legend设置
        opts_legend = opts.LegendOpts(
            item_width=15, border_color='transparent', pos_left='center', pos_top='8%', orient='horizontal',
            textstyle_opts=opts.TextStyleOpts(color='black', font_size=16, font_weight='bold'),
        )

        # 全局设置
        bar_city.set_global_opts(
            title_opts=opts.TitleOpts(title=city, pos_left='center', title_textstyle_opts=opts.TextStyleOpts(color='black')),  # 标题设置
            yaxis_opts=opts_yaxis_left,
            datazoom_opts=opts.DataZoomOpts(is_show=False, type_='slider', range_start=0, range_end=100),
            toolbox_opts=opts_toolbox,
            legend_opts=opts_legend,
            xaxis_opts=opts_xaxis_bottom,
            # visualmap_opts=opts.VisualMapOpts(is_inverse=True),

        )

        # bar添加至page中
        page.add(bar_city)

    # 渲染页面
    page.render(path_html)

    # 打开图片
    webbrowser.open(path_html)


def aqi_bar_by_city_monthly(data_: dict, path_html: str):
    """ AQI分城市及污染程度统计-基于pyecharts库

        data_: key为城市名, value为df, df的index为年-月, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-06
    """

    # 创建一个Page实例，将多个图表放在同一页上
    page = Page(layout=Page.SimplePageLayout, page_title='AQI Bar-Month', is_remove_br=True)

    """ 选项 """
    # 底部x轴选项
    opts_xaxis_bottom = opts.AxisOpts(
        position='bottom',
        axislabel_opts=opts.LabelOpts(
            rotate=90,
            horizontal_align='right',
            margin=10,
            font_weight='bold',
            font_family='Microsoft YaHei'
        ),
        axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=False),
    )

    # 顶部x轴选项
    # opts_xaxis_top = opts.AxisOpts(
    #     axisline_opts=opts.AxisLineOpts(is_on_zero=False, linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
    #     axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
    # )

    # 左y轴选项
    opts_yaxis_left = opts.AxisOpts(
        type_="value",
        name="Percentage (%)",
        name_location='center',
        name_gap=40,
        min_=0,
        max_=100.2,
        axislabel_opts=opts.LabelOpts(font_weight='bold', font_family='Microsoft YaHei'),
        axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')),
        axistick_opts=opts.AxisTickOpts(is_align_with_label=True),
        splitline_opts=opts.SplitLineOpts(is_show=False),
        name_textstyle_opts=opts.TextStyleOpts(font_weight='bold', font_family='Microsoft YaHei'),
        minor_tick_opts=opts.MinorTickOpts(is_show=True, split_number=2),
    )

    # 右y轴选项
    # opts_yaxis_right = opts.AxisOpts(axisline_opts=opts.AxisLineOpts(linestyle_opts=opts.LineStyleOpts(width=1, color='black')))

    # 按城市作图
    for city in data_.keys():

        # 创建一个 Bar 实例

        # bar_city = Bar()
        bar_city = Bar(init_opts=opts.InitOpts(width='1900px', height='360px', is_horizontal_center=True))

        # 添加x轴数据
        bar_city.add_xaxis(xaxis_data=data_[city].index.astype(str).tolist())

        # 默认保存文件名
        png_name = 'AQI_' + city

        """ 添加不同污染程度的数据 """
        # 添加数据
        for i in range(len(cfg.list_pollution_level)):
            bar_city.add_yaxis(series_name=cfg.list_pollution_level[i],
                               y_axis=data_[city].iloc[:, i].astype(str).tolist(),
                               stack='stack_' + city,
                               color=cfg.list_color_pollution_level[i],
                               bar_width='100%',
                               )

        # 添加顶部x轴
        # bar_city.extend_axis(xaxis=opts_xaxis_top)

        # 添加右y轴
        # bar_city.extend_axis(yaxis=opts_yaxis_right)

        # ToolboxOpts工具箱选项
        opts_toolbox = opts.ToolboxOpts(
            is_show=True,
            orient='horizontal',
            pos_left='92%',
            pos_top='2%',
            feature=opts.ToolBoxFeatureOpts(
                save_as_image=opts.ToolBoxFeatureSaveAsImageOpts(
                    type_='png',
                    background_color='rgba(255, 255, 255, 0)',
                    name=png_name,
                    pixel_ratio=4,
                ),
                restore=opts.ToolBoxFeatureRestoreOpts(is_show=False),
                data_view=opts.ToolBoxFeatureDataViewOpts(is_show=False),
                data_zoom=opts.ToolBoxFeatureDataZoomOpts(is_show=False),
                magic_type=opts.ToolBoxFeatureMagicTypeOpts(is_show=False),
            ),
        )

        # 序列label选项
        bar_city.set_series_opts(label_opts=opts.LabelOpts(is_show=False))

        # legend设置
        opts_legend = opts.LegendOpts(
            item_width=15, border_color='transparent', pos_left='center', pos_top='8%', orient='horizontal',
            textstyle_opts=opts.TextStyleOpts(color='black', font_size=16, font_weight='bold'),
        )

        # 全局设置
        bar_city.set_global_opts(
            title_opts=opts.TitleOpts(title=city, pos_left='center', title_textstyle_opts=opts.TextStyleOpts(color='black')),  # 标题设置
            yaxis_opts=opts_yaxis_left,
            datazoom_opts=opts.DataZoomOpts(is_show=False, type_='slider', range_start=0, range_end=100),
            toolbox_opts=opts_toolbox,
            legend_opts=opts_legend,
            xaxis_opts=opts_xaxis_bottom,
            # visualmap_opts=opts.VisualMapOpts(is_inverse=True),

        )

        # bar添加至page中
        page.add(bar_city)

    # 渲染页面
    page.render(path_html)

    # 打开图片
    webbrowser.open(path_html)


def aqi_bar_monthly_bokeh(data_:pd.DataFrame, path_html: str, title: str, show_html=True, width=1880, height=300):
    """ AQI分城市及污染程度统计-基于bokeh库

        data_: key为城市名, value为df, df的index为月分辨率的DataTimeIndex, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-09
    """

    # 准备factors
    factors = [(dt.strftime('%Y'), str(dt.month)) for dt in data_.index]

    # 数据转换为字典
    dict_data = data_.to_dict(orient='list')
    dict_data.update({'x': factors})

    # 污染级别列表
    columns = data_.columns.tolist()

    # 转换数据结构
    source = ColumnDataSource(data=dict_data)

    # 画布
    p = figure(x_range=FactorRange(*factors),
               height=height, width=width,
               tools='save,reset',
               toolbar_location='right',
               title_location='above',
               )

    # 画布透明
    p.background_fill_color = None
    p.border_fill_color = None

    # 堆叠图
    p.vbar_stack(columns,
                 x='x',
                 width=1,
                 alpha=1,
                 # color=['#66c430', '#e9da2e', '#f57217', '#ee1c25', '#66247b', '#8a2327'],
                 color=cfg.list_color_pollution_level,
                 source=source,
                 legend_label=columns,
                 # line_color='red',
                 line_width=0,
                 # hatch_color='blue',
                 )

    # 年份列表
    list_year = data_.index.year.unique().tolist()

    # 在不同组之间绘制竖直线
    n = 0
    for year in list_year:

        # 当年的月份列表
        index_current_year = data_.index[data_.index.year == year]

        # 横坐标
        n += index_current_year.shape[0]

        # 添加竖线
        p.line([n, n], [0, 100], line_width=1, line_color="black")

    # 外边框
    p.outline_line_color = None

    # y轴范围0-100
    p.y_range.start = 0
    p.y_range.end = 100

    # 组间距（以年分组）：0-1
    p.x_range.range_padding = 0

    # 不同组之间的间距: 0-1
    p.x_range.group_padding = 0

    # axis label方向
    p.xaxis.major_label_orientation = 'horizontal'

    # grid line
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    # legend
    # p.legend.background_fill_color = None
    p.legend.border_line_color = None
    p.legend.margin = 5
    p.legend.location = "top_center"
    p.legend.orientation = "horizontal"
    p.legend.label_text_font_size = '12pt'
    p.legend.label_text_color = 'black'

    p.add_layout(p.legend[0], 'above')

    # axis title
    p.yaxis.axis_label = 'Percentage (%)'

    # axis tick
    p.xaxis.axis_line_cap = 'square'
    p.yaxis.major_tick_line_width = 1
    p.yaxis.major_tick_line_cap = 'square'
    p.yaxis.minor_tick_line_width = 1
    p.yaxis.minor_tick_line_cap = 'square'
    p.yaxis.major_tick_in = 0

    p.xaxis.major_tick_line_width = 1
    p.xaxis.major_tick_line_cap = 'square'
    # p.xaxis.major_tick_line_color = 'grey'
    p.xaxis.major_tick_in = 0

    # axis label
    p.yaxis.axis_label_text_color = 'black'
    p.yaxis.axis_label_standoff = 5     # axis标题与axis的距离
    p.yaxis.axis_label_text_font_size = '12pt'
    p.yaxis.axis_label_text_font_style = 'bold'

    # ticklabels
    p.yaxis.major_label_text_font_size = '10pt'
    p.yaxis.major_label_text_color = 'black'
    p.yaxis.major_label_text_font_style = 'bold'
    p.xaxis.major_label_text_font_size = '8pt'
    p.xaxis.major_label_text_color = 'black'

    # axis line
    p.xaxis.axis_line_width = 1
    p.yaxis.axis_line_width = 1

    # axis ticker
    p.yaxis.ticker = SingleIntervalTicker(interval=20, num_minor_ticks=2)

    # group label
    p.xaxis.group_text_font_size = "12pt"
    p.xaxis.group_text_color = "black"
    p.xaxis.group_text_font_style = "bold"

    # figure title
    p.title.text = title
    p.title.text_font_size = '16pt'
    p.title.text_color = 'black'
    p.title.align = 'center'
    p.title.standoff = 0
    p.title.vertical_align = 'bottom'

    if show_html:

        # 输出html文件
        output_file(filename=path_html, title='AQI Bar-Month')

        # 打开html文件
        show(p)

    else:
        return p


def aqi_bar_seasonal_bokeh(data_:pd.DataFrame, path_html: str, title: str, show_html=True, width=900, height=300):
    """ AQI分城市及污染程度统计-基于bokeh库

        data_: key为城市名, value为df, df的index为季节分辨率的DataTimeIndex, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-10
    """

    # 月份与季节对应字典
    dict_month2season = {3:'春', 6: '夏', 9: '秋', 12: '冬'}

    # 准备factors
    factors = [(dt.strftime('%Y'), dict_month2season[dt.month]) for dt in data_.index]

    # 数据转换为字典
    dict_data = data_.to_dict(orient='list')
    dict_data.update({'x': factors})

    # 污染级别列表
    columns = data_.columns.tolist()

    # 转换数据结构
    source = ColumnDataSource(data=dict_data)

    # 画布
    p = figure(x_range=FactorRange(*factors),
               height=height, width=width,
               tools='save,reset',
               toolbar_location='right',
               title_location='above',
               )

    # 画布透明
    p.background_fill_color = None
    p.border_fill_color = None

    # 堆叠图
    p.vbar_stack(columns,
                 x='x',
                 width=1,
                 alpha=1,
                 # color=['#66c430', '#e9da2e', '#f57217', '#ee1c25', '#66247b', '#8a2327'],
                 color=cfg.list_color_pollution_level,
                 source=source,
                 legend_label=columns,
                 # line_color='red',
                 line_width=0,
                 # hatch_color='blue',
                 )

    # 年份列表
    list_year = data_.index.year.unique().tolist()

    # 在不同组之间绘制竖直线
    n = 0
    for year in list_year:

        # 当年的月份列表
        index_current_year = data_.index[data_.index.year == year]

        # 横坐标
        n += index_current_year.shape[0]

        # 添加竖线
        p.line([n, n], [0, 100], line_width=1, line_color="black")

    # 外边框
    p.outline_line_color = None

    # y轴范围0-100
    p.y_range.start = 0
    p.y_range.end = 100

    # 组间距（以年分组）：0-1
    p.x_range.range_padding = 0

    # 不同组之间的间距: 0-1
    p.x_range.group_padding = 0

    # axis label方向
    p.xaxis.major_label_orientation = 'horizontal'

    # grid line
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    # legend
    # p.legend.background_fill_color = None
    p.legend.border_line_color = None
    p.legend.margin = 5
    p.legend.location = "top_center"
    p.legend.orientation = "horizontal"
    p.legend.label_text_font_size = '12pt'
    p.legend.label_text_color = 'black'

    p.add_layout(p.legend[0], 'above')

    # axis title
    p.yaxis.axis_label = 'Percentage (%)'

    # axis tick
    p.xaxis.axis_line_cap = 'square'
    p.yaxis.major_tick_line_width = 1
    p.yaxis.major_tick_line_cap = 'square'
    p.yaxis.minor_tick_line_width = 1
    p.yaxis.minor_tick_line_cap = 'square'
    p.yaxis.major_tick_in = 0

    p.xaxis.major_tick_line_width = 1
    p.xaxis.major_tick_line_cap = 'square'
    # p.xaxis.major_tick_line_color = 'grey'
    p.xaxis.major_tick_in = 0

    # axis label
    p.yaxis.axis_label_text_color = 'black'
    p.yaxis.axis_label_standoff = 5     # axis标题与axis的距离
    p.yaxis.axis_label_text_font_size = '12pt'
    p.yaxis.axis_label_text_font_style = 'bold'

    # ticklabels
    p.yaxis.major_label_text_font_size = '10pt'
    p.yaxis.major_label_text_color = 'black'
    p.yaxis.major_label_text_font_style = 'bold'
    p.xaxis.major_label_text_font_size = '8pt'
    p.xaxis.major_label_text_color = 'black'

    # axis line
    p.xaxis.axis_line_width = 1
    p.yaxis.axis_line_width = 1

    # axis ticker
    p.yaxis.ticker = SingleIntervalTicker(interval=20, num_minor_ticks=2)

    # group label
    p.xaxis.group_text_font_size = "12pt"
    p.xaxis.group_text_color = "black"
    p.xaxis.group_text_font_style = "bold"

    # figure title
    p.title.text = title
    p.title.text_font_size = '16pt'
    p.title.text_color = 'black'
    p.title.align = 'center'
    p.title.standoff = 0
    p.title.vertical_align = 'bottom'

    if show_html:

        # 输出html文件
        output_file(filename=path_html, title='AQI Bar-Season')

        # 打开html文件
        show(p)

    else:
        return p


def primary_pollutant_bokeh(data_:pd.DataFrame, path_html: str, title: str, show_html=True, width=1880, height=300):
    """ 首要污染物统计-基于bokeh库

        data_: index为月分辨率DateTimeIndex, columns为['O3', 'SO2', 'NO2', 'CO', 'PM10', 'PM2.5']
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-10
    """
    import numpy as np

    # 物种顺序
    list_species = ['O3', 'SO2', 'NO2', 'CO', 'PM10', 'PM2.5', 'AQI≤50']

    # 表头顺序调整
    list_header = [i for i in list_species if i in data_.columns]
    data_ = data_.loc[:, list_header]

    # 数据中的nan转换为0
    data_.fillna(0, inplace=True)

    # 颜色列表
    dict_color = dict(zip(list_species, cfg.list_color_primary_pollutant))
    list_color = [dict_color[k] for k in list_header]

    # 准备factors
    factors = [(dt.strftime('%Y'), str(dt.month)) for dt in data_.index]

    # 数据转换为字典
    dict_data = data_.to_dict(orient='list')
    dict_data.update({'x': factors})

    # 首要污染物列表
    # columns = data_.columns.tolist()
    # print(columns)

    # 转换数据结构
    source = ColumnDataSource(data=dict_data)

    # 画布
    p = figure(x_range=FactorRange(*factors),
               height=height, width=width,
               tools='save,reset',
               toolbar_location='right',
               title_location='above',
               )
    
    # 画布透明
    p.background_fill_color = None
    p.border_fill_color = None

    # 堆叠图
    p.vbar_stack(list_header,
                 x='x',
                 width=1,
                 alpha=1,
                 # color=['#66c430', '#e9da2e', '#f57217', '#ee1c25', '#66247b', '#8a2327'],
                 color=list_color,
                 source=source,
                 legend_label=list_header,
                 # line_color='red',
                 line_width=0,
                 # hatch_color='blue',
                 )

    # 年份列表
    list_year = data_.index.year.unique().tolist()

    # 在不同组之间绘制竖直线
    n = 0
    for year in list_year:

        # 当年的月份列表
        index_current_year = data_.index[data_.index.year == year]

        # 横坐标
        n += index_current_year.shape[0]

        # 添加竖线
        p.line([n, n], [0, 100], line_width=1, line_color="black")

    # 外边框
    p.outline_line_color = None

    # y轴范围0-100
    p.y_range.start = 0
    p.y_range.end = 100

    # 组间距（以年分组）：0-1
    p.x_range.range_padding = 0

    # 不同组之间的间距: 0-1
    p.x_range.group_padding = 0

    # axis label方向
    p.xaxis.major_label_orientation = 'horizontal'

    # grid line
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    # legend
    # p.legend.background_fill_color = None
    p.legend.border_line_color = None
    p.legend.margin = 5
    p.legend.location = "top_center"
    p.legend.orientation = "horizontal"
    p.legend.label_text_font_size = '12pt'
    p.legend.label_text_color = 'black'

    p.add_layout(p.legend[0], 'above')

    # axis title
    p.yaxis.axis_label = 'Percentage (%)'

    # axis tick
    p.xaxis.axis_line_cap = 'square'
    p.yaxis.major_tick_line_width = 1
    p.yaxis.major_tick_line_cap = 'square'
    p.yaxis.minor_tick_line_width = 1
    p.yaxis.minor_tick_line_cap = 'square'
    p.yaxis.major_tick_in = 0

    p.xaxis.major_tick_line_width = 1
    p.xaxis.major_tick_line_cap = 'square'
    # p.xaxis.major_tick_line_color = 'grey'
    p.xaxis.major_tick_in = 0

    # axis label
    p.yaxis.axis_label_text_color = 'black'
    p.yaxis.axis_label_standoff = 5     # axis标题与axis的距离
    p.yaxis.axis_label_text_font_size = '12pt'
    p.yaxis.axis_label_text_font_style = 'bold'

    # ticklabels
    p.yaxis.major_label_text_font_size = '10pt'
    p.yaxis.major_label_text_color = 'black'
    p.yaxis.major_label_text_font_style = 'bold'
    p.xaxis.major_label_text_font_size = '8pt'
    p.xaxis.major_label_text_color = 'black'

    # axis line
    p.xaxis.axis_line_width = 1
    p.yaxis.axis_line_width = 1

    # axis ticker
    p.yaxis.ticker = SingleIntervalTicker(interval=20, num_minor_ticks=2)

    # group label
    p.xaxis.group_text_font_size = "12pt"
    p.xaxis.group_text_color = "black"
    p.xaxis.group_text_font_style = "bold"

    # figure title
    p.title.text = title
    p.title.text_font_size = '16pt'
    p.title.text_color = 'black'
    p.title.align = 'center'
    p.title.standoff = 0
    p.title.vertical_align = 'bottom'

    if show_html:

        # 输出html文件
        output_file(filename=path_html, title='Primary Pollutant')

        # 打开html文件
        show(p)

    else:
        return p


def aqi_bar_by_city_monthly_bokeh(data_: dict, path_html: str):
    """ AQI分城市及污染程度统计-基于bokeh库

        data_: key为城市名, value为df, df的index为年-月, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-09
    """

    # figure添加至list
    list_figure = []

    # 分城市作图
    for city in data_.keys():

        # 利用bokeh作图
        figure_city = aqi_bar_monthly_bokeh(data_=data_[city], path_html=path_html, title=city, show_html=False, width=1880, height=300)

        # 添加至list
        list_figure.append(figure_city)

    # 添加至列Layout
    layout = gridplot(list_figure, sizing_mode='stretch_width', ncols=1, merge_tools=False, toolbar_location=None)

    # 输出html文件
    output_file(filename=path_html, title='AQI Bar-Month')

    # 打开html文件
    show(layout)


def aqi_bar_by_city_seasonal_bokeh(data_: dict, path_html: str):
    """ AQI分城市及污染程度统计-基于bokeh库

        data_: key为城市名, value为df, df的index为DateTimeIndex, columns为污染级别: '优', '良', '轻度污染', '中度污染', '重度污染', '严重污染'
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-10
    """

    # figure添加至list
    list_figure = []

    # 分城市作图
    for city in data_.keys():

        # 利用bokeh作图
        figure_city = aqi_bar_seasonal_bokeh(data_=data_[city], path_html=path_html, title=city, show_html=False, width=900, height=300)

        # 添加至list
        list_figure.append(figure_city)

    # 添加至列Layout, grid, 2列
    layout_ = gridplot(list_figure, ncols=2, sizing_mode='stretch_width', merge_tools=False, toolbar_location=None)

    # 输出html文件
    output_file(filename=path_html, title='AQI Bar-Season')

    # 打开html文件
    show(layout_)


class PlotPrimaryPollutantByCity(QThread):
    """ 首要污染物统计, 分城市作图-QThread调取plot_primary_pollutant_by_city_bokeh

        data_: key为城市名, value为df, df的index为月分辨率DateTimeIndex, columns为['O3', 'SO2', 'NO2', 'CO', 'PM10', 'PM2.5']
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-11
    """

    signal_progress = pyqtSignal(dict)

    def __init__(self, data_, path_html):
        super().__init__()

        self.data = data_
        self.path_html = path_html

    def run(self):

        # 发送状态信号
        self.signal_progress.emit({'text': '作图中...', 'value': None})

        plot_primary_pollutant_by_city_bokeh(data_=self.data, path_html=self.path_html)

        # 发送状态信号
        self.signal_progress.emit({'text': '作图完成！', 'value': 1000})
        

def plot_primary_pollutant_by_city_bokeh(data_: dict, path_html: str):
    """ 首要污染物统计, 分城市作图

        data_: key为城市名, value为df, df的index为月分辨率DateTimeIndex, columns为['O3', 'SO2', 'NO2', 'CO', 'PM10', 'PM2.5']
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-04
    """
    
    # figure添加至list
    list_figure = []
    # print(data_)

    # 分城市作图
    for city in data_.keys():

        # 利用bokeh作图
        figure_city = primary_pollutant_bokeh(data_=data_[city], path_html=path_html, title=city, show_html=False, width=1880, height=300)

        # 添加至list
        list_figure.append(figure_city)

    # 添加至列Layout, grid, 2列
    layout_ = gridplot(list_figure, ncols=1, merge_tools=False, sizing_mode='stretch_width', toolbar_location=None)

    # 输出html文件
    output_file(filename=path_html, title='AQI Bar-Season')

    # 打开html文件
    show(layout_)
    

def geo_china(data_: pd.DataFrame | pd.Series, coords_: pd.DataFrame, pieces_: list, color_bg='black', path_html="Geo-China.html", precision=0):
    """ Geo图制作

        data_: pd.DataFrame, 索引为坐标点名称，每一列数据单独做一个图，每一列的列名作为图的标题
                pd.Series, 索引同上，数据作图，数据名作为图的标题

        coords_: pd.DtaFrame, 索引同上，第一列为经度，第二列为纬度

        pieces: 数据按范围指定颜色, 如:
            pieces_pm2p5 = [
                {'lte': 5, 'color': '#0000ff'},
                {'gte': 6, 'lte': 20, 'color': '#00ffff'},
                {'gte': 21, 'lte': 35, 'color': '#00ff00'},
                {'gte': 36, 'lte': 55, 'color': '#ffff00'},
                {'gte': 56, 'lte': 75, 'color': '#ff8000'},
                {'gte': 76, 'lte': 100, 'color': '#ff0000'},
                {'gte': 101, 'color': '#ff00ff'},
            ]

        color_bg: 地图背景色, 支持透明'transparent'

        path_html: 作图保存路径
        precision: colormap的显示精度, 小数位数


    无返回值
    2023-09-07
    """

    # 判断data_数据类型
    if isinstance(data_, pd.Series):
        df_data = data_.to_frame()
    elif isinstance(data_, pd.DataFrame):
        df_data = data_
    else:
        raise TypeError('仅支持pandas.DataFrame和pd.Series, 输入: %s' % type(data_))

    # 选项-VisualMapOpts
    opts_visual_map = opts.VisualMapOpts(
        orient='vertical',
        is_inverse=False,
        item_width=20,
        item_height=10,
        is_piecewise=True,
        range_opacity=1,
        pieces=pieces_,
        textstyle_opts=opts.TextStyleOpts(font_size=11, font_weight='bold', color='black'),
        pos_bottom='25%',
        pos_right='5%',
        precision=precision,
    )

    # 创建一个Page实例，将多个图表放在同一页上
    page = Page(layout=Page.SimplePageLayout, page_title='Geo_China', is_remove_br=True)

    # 按日期作图
    for dt in df_data.columns:

        # 提取数据
        series_data = df_data.loc[:, dt]

        # 去除nan
        series_data.dropna(inplace=True)

        # 取整
        if precision == 0:
            series_data = series_data.round(precision).astype(int)
        else:
            series_data = series_data.round(precision)

        # 创建Geo实例
        geo_china = Geo(init_opts=opts.InitOpts(width='480px', height='380px'))

        # 地图类型
        geo_china.add_schema(
            maptype='china',
            is_roam=False,
            center=(106, 35),
            zoom=1.6,
            selected_mode='multiple',
            itemstyle_opts=opts.ItemStyleOpts(border_color="grey", border_width=0.25, area_color=color_bg),
        )

        # 站点列表
        list_site = series_data.index

        # 添加坐标数据
        for site in list_site:

            # 准备数据(site, lon, lat)
            tuple_site = (site, *coords_.loc[site, :])

            # 添加数据至Geo
            geo_china.add_coordinate(*tuple_site)

        # 准备数据
        list_tuple = [(index, value) for index, value in series_data.items()]

        # 增加数据至Geo, 指定作图类型, 标记大小
        geo_china.add(
            series_name=dt,
            data_pair=list_tuple,
            type_=GeoType.SCATTER,
            symbol='circle',
            symbol_size=cfg.scatter_size_geo,
        )

        # 默认保存路径
        png_name = 'Geo_China_' + str(dt)

        # ToolboxOpts工具箱选项
        opts_toolbox = opts.ToolboxOpts(
            is_show=True,
            orient='horizontal',
            pos_left='95%',
            pos_top='1%',
            feature=opts.ToolBoxFeatureOpts(
                save_as_image=opts.ToolBoxFeatureSaveAsImageOpts(
                    type_='png',
                    background_color='rgba(255, 255, 255, 0)',
                    name=png_name,
                    pixel_ratio=2,
                ),
                restore=opts.ToolBoxFeatureRestoreOpts(is_show=False),
                data_view=opts.ToolBoxFeatureDataViewOpts(is_show=False),
                data_zoom=opts.ToolBoxFeatureDataZoomOpts(is_show=False),
                magic_type=opts.ToolBoxFeatureMagicTypeOpts(is_show=False),
            ),
        )

        # 隐藏标记值
        geo_china.set_series_opts(label_opts=opts.LabelOpts(is_show=False))

        # 全局设置
        geo_china.set_global_opts(
            title_opts=opts.TitleOpts(
                title=str(dt),
                title_textstyle_opts=opts.TextStyleOpts(font_size=20, color='black'),
                pos_left='center', pos_top='10%',
            ),
            visualmap_opts=opts_visual_map,
            toolbox_opts=opts_toolbox,
            legend_opts=opts.LegendOpts(is_show=False),
        )

        # 添加值Page
        page.add(geo_china)

    # 渲染页面
    page.render(path_html)

    # 打开图片
    webbrowser.open(path_html)


def ts_1p_bokeh(data_: pd.DataFrame | pd.Series, path_html: str, title: str, show_html=True, width=1880, height=400):
    """ 时间序列数据作图-基于bokeh库

        data_: index为DateTimeIndex, columns为不同区域(省/市/站点)名称
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-11
    """
    
    # 判断data_数据类型
    if isinstance(data_, pd.Series):
        df_data = data_.to_frame()
    elif isinstance(data_, pd.DataFrame):
        df_data = data_
    else:
        raise TypeError('仅支持pandas.DataFrame和pd.Series, 输入: %s' % type(data_))

    # 列名
    list_header = df_data.columns.tolist()

    # 颜色列表
    if len(list_header) <= 10:
        list_colors = Category10[10]
    elif len(list_header) <= 20:
        list_colors = Category20[20]
    else:
        list_colors = linear_palette(Turbo256, len(list_header))
    # 画布
    p = figure(
               height=height, width=width,
               tools='box_zoom,save,reset,help',
            #    tools='save,reset',
               toolbar_location='right',
               title_location='above',
               )
    
    # 画布透明
    p.background_fill_color = None
    p.border_fill_color = None
    
    # 将DateTimeIndex转换为 Pandas Series
    x = df_data.index.to_series()

    n = 0
    for col in list_header:

        # 添加数据并绘制点线图
        y = df_data[col]

        # p.line(x, y, line_width=2, line_color="blue", legend_label="线条1")

        p.circle(x, y, legend_label=col, fill_color=list_colors[n], line_color=list_colors[n])
        p.line(x, y, legend_label=col, line_color=list_colors[n])

        n += 1

    # 设置横坐标轴的日期时间格式
    p.xaxis.ticker = DatetimeTicker(desired_num_ticks=10)
    p.xaxis.formatter = DatetimeTickFormatter(hours='%Y/%m/%d %H:%M', days='%Y/%m/%d', months='%Y/%m', years='%Y')

    # 外边框
    p.outline_line_color = None

    # y轴范围0-100
    p.y_range.start = 0
    # p.y_range.end = 100

    # x轴两边数据距离y轴的间隙: 0-1
    p.x_range.range_padding = 0.001

    # axis label方向
    p.xaxis.major_label_orientation = 'horizontal'

    # grid line
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    # legend
    # p.legend.background_fill_color = None
    p.legend.border_line_color = None
    p.legend.margin = 5
    p.legend.location = "top_center"
    p.legend.orientation = "horizontal"
    p.legend.label_text_font_size = '12pt'
    p.legend.label_text_color = 'black'
    p.legend.click_policy = "hide"

    p.add_layout(p.legend[0], 'above')

    # axis title
    p.yaxis.axis_label = 'Concentration'

    # axis tick
    p.xaxis.axis_line_cap = 'square'
    p.yaxis.major_tick_line_width = 1
    p.yaxis.major_tick_line_cap = 'square'
    p.yaxis.minor_tick_line_width = 1
    p.yaxis.minor_tick_line_cap = 'square'
    p.yaxis.major_tick_in = 0

    p.xaxis.major_tick_line_width = 1
    p.xaxis.major_tick_line_cap = 'square'
    # p.xaxis.major_tick_line_color = 'grey'
    p.xaxis.major_tick_in = 0

    # axis label
    p.yaxis.axis_label_text_color = 'black'
    p.yaxis.axis_label_standoff = 5     # axis标题与axis的距离
    p.yaxis.axis_label_text_font_size = '12pt'
    p.yaxis.axis_label_text_font_style = 'bold'

    # ticklabels
    p.yaxis.major_label_text_font_size = '10pt'
    p.yaxis.major_label_text_color = 'black'
    p.yaxis.major_label_text_font_style = 'bold'
    p.xaxis.major_label_text_font_size = '10pt'
    p.xaxis.major_label_text_color = 'black'
    p.xaxis.major_label_text_font_style = 'bold'

    # axis line
    p.xaxis.axis_line_width = 1
    p.yaxis.axis_line_width = 1

    # axis ticker
    # p.yaxis.ticker = SingleIntervalTicker(interval=20, num_minor_ticks=2)

    # figure title
    p.title.text = title
    p.title.text_font_size = '16pt'
    p.title.text_color = 'black'
    p.title.align = 'center'
    p.title.standoff = 0
    p.title.vertical_align = 'bottom'

    if show_html:

        # 输出html文件
        output_file(filename=path_html, title='TimeSeries')

        # 打开html文件
        show(p)

    else:
        return p


def ts_bokeh(data_: dict, path_html: str):
    """ 时间序列作图-基于bokeh库

        data_: key为物种或地域名, value为df, df的index为DateTimeIndex, columns为地域名或物种名
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-11
    """

    # figure添加至list
    list_figure = []

    # 分城市作图
    for city in data_.keys():

        # 利用bokeh作图
        figure_city = ts_1p_bokeh(data_=data_[city], path_html=path_html, title=city, show_html=False, width=1880, height=400)

        # 添加至list
        list_figure.append(figure_city)

    # 添加至列Layout, grid, 2列
    layout_ = gridplot(list_figure, ncols=1, sizing_mode='stretch_width', merge_tools=False, toolbar_location=None)

    # 输出html文件
    output_file(filename=path_html, title='TimeSeries')

    # 打开html文件
    show(layout_)


def bar_year_bokeh(data_: dict, path_html: str):
    """ 时间序列作图-基于bokeh库

        data_: key为级别1名称, value为df, df的index为DateTimeIndex, columns为级别2名称
        path_html: 作图文件默认保存路径

    无返回值
    2023-09-13
    """

    # 级别1列表
    list_level1 = list(data_.keys())

    # 级别2列表
    list_level2 = data_[list_level1[0]].columns.tolist()

    # 按级别2分配颜色
    if len(list_level2) <= 10:
        list_colors = Category10[10]

    elif len(list_level2) <= 20:
        list_colors = Category20[20]
    else:
        list_colors = linear_palette(Turbo256, len(list_level2))

    # figure添加至list
    list_figure = []

    # 按级别1分类
    for i in list_level1:
        
        # 包含的级别2名称列表
        # list_level2_i = data_[i].columns.tolist()

        # 按级别2分裂
        n = 0
        for j in list_level2:
            
            # png默认路径
            # path_png_j = os.path.join(os.path.split(path_html)[0], '%s | %s' % (i, j) + '.png')

            # 利用bokeh作图
            figure_i_j = bar_year_1p_bokeh(data_=data_[i].loc[:, j], 
                                           path_html=path_html, 
                                        #    path_png=path_png_j,
                                           title='%s | %s' % (i, j), 
                                           show_html=False, 
                                           color=list_colors[n], 
                                           width=400, height=300,
                                           )

            # 添加至list
            list_figure.append(figure_i_j)
        
            n += 1

    # 参考y轴
    list_figure_ref_y = list_figure[: len(list_level2)]
 
    # 级别2名称相同的共享y轴
    list_figure_share_y = []
    for num in range(len(list_figure)):
        index_p = num % len(list_level2)
        p = list_figure[num]
        p.y_range = list_figure_ref_y[index_p].y_range

        list_figure_share_y.append(p)

    # 共享y轴
    list_figure_share_xy = []
    for p in list_figure_share_y:
        p.x_range = list_figure_share_y[0].x_range
        list_figure_share_xy.append(p)

    # 布局列数
    if len(list_level2) == 1:
        ncols= 6
    else:
        ncols = len(list_level2)

    # 添加至列Layout, grid, 5列
    layout_ = gridplot(list_figure_share_xy, ncols=ncols, sizing_mode='stretch_width', merge_tools=False, toolbar_location=None)

    # 输出html文件
    output_file(filename=path_html, title='Bar_Annual')

    # 打开html文件
    show(layout_)


def bar_year_1p_bokeh(data_: pd.Series, path_html: str, title: str,show_html=True, color='orange', width=400, height=300):
    """ 年均值数据作图-基于bokeh库-柱状图

        data_: index为DateTimeIndex
        path_html: 作图文件默认保存路径
        path_png: png文件默认保存路径

    无返回值
    2023-09-13
    """

    # x值
    data_x = data_.index.year.to_numpy()

    # y值
    data_y = data_.to_numpy()

    # 画布
    p = figure(
               height=height, width=width,
               tools='box_zoom,save,reset,help',
               toolbar_location='right',
               title_location='above',
               )
    
    # 画布透明
    p.background_fill_color = None
    p.border_fill_color = None

    # plot
    p.vbar(x=data_x, top=data_y, width=0.7, color=color)

    # 外边框
    p.outline_line_color = None

    # y轴范围0-100
    p.y_range.start = 0
    # p.y_range.end = 100

    # x轴两边数据距离y轴的间隙: 0-1
    # p.x_range.range_padding = 0.1

    # axis label方向
    p.xaxis.major_label_orientation = 0.8

    # grid line
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    # legend
    # p.legend.background_fill_color = None
    # p.legend.border_line_color = None
    # p.legend.margin = 5
    # p.legend.location = "top_center"
    # p.legend.orientation = "horizontal"
    # p.legend.label_text_font_size = '12pt'
    # p.legend.label_text_color = 'black'
    # p.legend.click_policy = "hide"

    # p.add_layout(p.legend[0], 'above')

    # axis title
    p.yaxis.axis_label = 'Concentration'

    # axis tick
    p.xaxis.axis_line_cap = 'square'
    p.yaxis.major_tick_line_width = 1
    p.yaxis.major_tick_line_cap = 'square'
    p.yaxis.minor_tick_line_width = 1
    p.yaxis.minor_tick_line_cap = 'square'
    p.yaxis.major_tick_in = 0

    p.xaxis.major_tick_line_width = 1
    p.xaxis.major_tick_line_cap = 'square'
    # p.xaxis.major_tick_line_color = 'grey'
    p.xaxis.major_tick_in = 0

    # axis label
    p.yaxis.axis_label_text_color = 'black'
    p.yaxis.axis_label_standoff = 5     # axis标题与axis的距离
    p.yaxis.axis_label_text_font_size = '12pt'
    p.yaxis.axis_label_text_font_style = 'bold'

    # ticklabels
    p.yaxis.major_label_text_font_size = '10pt'
    p.yaxis.major_label_text_color = 'black'
    p.yaxis.major_label_text_font_style = 'bold'
    p.xaxis.major_label_text_font_size = '10pt'
    p.xaxis.major_label_text_color = 'black'
    p.xaxis.major_label_text_font_style = 'bold'

    # axis line
    p.xaxis.axis_line_width = 1
    p.yaxis.axis_line_width = 1

    # axis ticker
    p.xaxis.ticker = SingleIntervalTicker(interval=1, num_minor_ticks=0)

    # figure title
    p.title.text = title
    p.title.text_font_size = '16pt'
    p.title.text_color = 'black'
    p.title.align = 'center'
    p.title.standoff = 0
    p.title.vertical_align = 'bottom'

    # 设置点击下载按钮时png的默认文件名
    # export_png(p, filename=path_png)

    if show_html:

        # 输出html文件
        output_file(filename=path_html, title='Bar_' + data_.name)

        # 打开html文件
        show(p)

    else:
        return p


def bar_num_of_days_over_standard_days_every_monthly_bokeh(data_: dict, path_html: str, height=300, legend_label=['PM₂.₅≥75', '35≤PM₂.₅<75']):
    """ 分城市分月份统计超标天数-基于bokeh库

        data_: index为月分辨率的DataTimeIndex, columns为不同城市名称
        path_html: 作图文件默认保存路径
        height: 画布高度

    无返回值
    2023-09-14
    """

    # figure添加至list
    list_figure = []

    # 分城市作图
    for city, df_city in data_.items():

        # 准备factors
        factors = [(dt.strftime('%Y'), str(dt.month)) for dt in df_city.index]

        # 数据转换为字典
        dict_data_city = {'x': factors, 'y1': df_city.iloc[:, 0].to_numpy(), 'y2': df_city.iloc[:, 1]. to_numpy()}

        # 转换数据
        source_city = ColumnDataSource(data=dict_data_city)

        # 画布
        p = figure(x_range=FactorRange(*factors),
                height=height, 
                # width=width,
                tools='save,reset',
                toolbar_location='right',
                title_location='above',
                background_fill_color=None,
                border_fill_color=None,
                outline_line_color=None,
                )
        
        # 作图        
        p.vbar_stack(
            stackers=['y1', 'y2'], 
            x='x', 
            width=0.5, 
            color=['#66c2a5', '#fc8d62'], 
            source=source_city,
            legend_label=df_city.columns.tolist(),
            line_width=1,
            )

        # figure title
        p.title.text = city
        p.title.text_font_size = '16pt'
        p.title.text_color = 'black'
        p.title.align = 'center'
        p.title.standoff = 0
        p.title.vertical_align = 'bottom'

        # 共享x、y轴范围
        if list_figure:
            p.x_range = list_figure[0].x_range
            p.y_range = list_figure[0].y_range

        # 设置y轴范围
        p.y_range.start = -0.2
        # p.y_range.end = 100

        # 组间距（以年分组）：0-1
        p.x_range.range_padding = 0

        # 不同组之间的间距: 0-1
        p.x_range.group_padding = 0

        # axis label方向
        p.xaxis.major_label_orientation = 'horizontal'

        # grid line
        p.xgrid.grid_line_color = None
        p.ygrid.grid_line_color = 'silver'
        p.ygrid.grid_line_width = 0.5
        # p.ygrid.grid_line_alpha = 0.5

        p.ygrid.minor_grid_line_color = "silver"
        p.ygrid.minor_grid_line_alpha = 0.25
        p.ygrid.minor_grid_line_dash = "dashed" # solid/dashed/dotted/dotdash/dashdot

        # legend
        p.legend.background_fill_color = None
        p.legend.border_line_color = None
        # p.legend.margin = 5
        p.legend.spacing = 20   # item间距
        # p.legend.click_policy = 'hide'  # legend可点击
        p.legend.location = "top_center"
        p.legend.orientation = "horizontal"
        p.legend.label_text_font_size = '12pt'
        p.legend.label_text_color = 'black'
        p.add_layout(p.legend[0], 'above')

        # axis title
        p.yaxis.axis_label = 'Days'

        # axis tick
        p.xaxis.axis_line_cap = 'square'
        p.yaxis.major_tick_line_width = 1
        p.yaxis.major_tick_line_cap = 'square'
        p.yaxis.minor_tick_line_width = 1
        p.yaxis.minor_tick_line_cap = 'square'
        p.yaxis.major_tick_in = 0

        p.xaxis.major_tick_line_width = 1
        p.xaxis.major_tick_line_cap = 'square'
        # p.xaxis.major_tick_line_color = 'grey'
        p.xaxis.major_tick_in = 0

        # axis label
        p.yaxis.axis_label_text_color = 'black'
        p.yaxis.axis_label_standoff = 5     # axis标题与axis的距离
        p.yaxis.axis_label_text_font_size = '12pt'
        p.yaxis.axis_label_text_font_style = 'bold'

        # ticklabels
        p.yaxis.major_label_text_font_size = '10pt'
        p.yaxis.major_label_text_color = 'black'
        p.yaxis.major_label_text_font_style = 'bold'
        p.xaxis.major_label_text_font_size = '8pt'
        p.xaxis.major_label_text_color = 'black'

        # axis line
        p.xaxis.axis_line_width = 1
        p.yaxis.axis_line_width = 1

        # axis ticker
        # p.yaxis.ticker = SingleIntervalTicker(interval=20, num_minor_ticks=2)

        # group label
        p.xaxis.group_text_font_size = "12pt"
        p.xaxis.group_text_color = "black"
        p.xaxis.group_text_font_style = "bold"

        # 添加至list
        list_figure.append(p)

    # 添加至列Layout, grid, 2列
    layout_ = gridplot(list_figure, ncols=1, sizing_mode='stretch_width', merge_tools=False, toolbar_location=None)

    # 输出html文件
    output_file(filename=path_html, title='TimeSeries')

    # 打开html文件
    show(layout_)

